昨天,我們成功用 App 完成了與 AI 的第一次對話。
現在,我們不只要 AI 跟我們「聊天」,更要讓 AI 成為我們的「創意工具」。今天的專案目標是當我在 App 中輸入一段文字描述,例如: 寧靜的森林早晨,我們的 AI 設計師就能自動為我生成一套代表這個情境的、專業的配色方案。
困惑點:如何讓 AI 回傳我想要的「格式」?這次,我需要的不是一段描述顏色的文章,而是程式可以讀懂的顏色代碼,例如 #2F4F4F
這種十六進位碼。更重要的是,我需要的是一組「三個」互相搭配的顏色,來分別扮演「主色」、「輔助色」、「強調色」。
AI 協作
我正在打造一個『AI 設計師』App。我需要 Gemini API 根據我給的文字描述,回傳一個剛好包含三個十六進位顏色代碼的 JSON 陣列。我該如何設計我的Prompt,才能確保 AI 總是回傳我想要的、格式固定的 JSON,而不是一段普通的文字描述?
AI 的回答:
打造一個「AI 設計師」App 是一個很棒的專案!為了確保 Gemini API 能夠穩定地回傳你所需要的 JSON 格式,關鍵在於在 Prompt 中明確且嚴格地定義輸出的格式和內容。
以下是如何設計 Prompt 的建議,以及一些專業考量:
Prompt 設計範例
你可以使用以下這段 Prompt 作為基礎,並根據你的應用場景進行調整。這個範例包含了幾個重要的結構化指令:
你是一個專業的色彩分析助理。 根據以下使用者提供的文字描述,生成一個 JSON 陣列,其中包含 3 個十六進位顏色代碼。 請嚴格遵守以下規則: 1. 輸出必須是一個 JSON 陣列。 2. 陣列中只能包含 3 個字串元素。 3. 每個字串元素必須是一個有效的 6 位數十六進位顏色代碼(例如:"#FFFFFF")。 4. 除了 JSON 陣列本身,不要包含任何額外的文字、解釋或註解。 使用者描述: [在此處插入你的使用者文字描述,例如:「一個充滿活力的海灘日落,帶有橘色、粉紅色和紫色的色調。」] 範例輸出: ["#F4A261", "#E76F51", "#2A9D8F"]
設計原理與重點說明
- 明確的角色設定(Role-playing):
- 在 Prompt 的開頭,將 Gemini 設定為一個具有特定職責的角色,例如「你是一個專業的色彩分析助理」。這有助於引導模型進入特定的思考模式,並專注於任務。
- 嚴格的格式指令(Strict Formatting Instructions):
- 這是最重要的部分。明確列出你對輸出格式的所有要求,例如:
- 「輸出必須是一個 JSON 陣列。」
- 「陣列中只能包含 3 個字串元素。」
- 「每個字串元素必須是一個有效的 6 位數十六進位顏色代碼。」
- 使用條列式清單 (
1.
,2.
,3.
) 可以讓指令更清晰,減少模型誤解的可能性。- 禁止額外內容(No Extra Text):
- 使用「除了...不要包含任何額外的文字、解釋或註解」這類語句來明確告訴模型,你只想要 JSON 陣列,不希望它回傳任何額外的自然語言,例如「這是一組為您設計的顏色...」這類話語。這對自動化處理非常關鍵。
- 提供範例(Few-shot Example):
- 在 Prompt 中提供一個「範例輸出」是一個非常有效的方法。這不僅能讓模型了解你想要的格式,還能展示一個實際的成功案例,讓它更容易模仿。即使你的指令已經很明確,範例也能顯著提升成功率。
專業建議
- 系統層級指令:對於更複雜或對格式要求極高的應用,除了使用者給予的 Prompt,你也可以考慮在 API 呼叫的設定中,使用 System Instructions(如果 API 支援)。這類指令通常會在每次對話開始前給予模型一個基礎的行為準則,比在每個 Prompt 中重複相同的格式要求更有效。
- 錯誤處理:儘管透過上述設計可以大幅提升成功率,但 AI 模型的輸出仍存在不可預測性。在你的 App 中,務必實作 robust 的錯誤處理機制。例如:
- JSON 解析檢查:在接收到 API 回應後,先檢查它是否能被正確解析為 JSON。
- 內容格式驗證:檢查陣列中的元素數量是否為 3,以及每個字串是否符合十六進位代碼的正規表示式(Regular Expression)。
- 如果驗證失敗,可以選擇重新發送請求,或者使用預設的顏色組合作為備援方案。
透過這些結構化且嚴格的 Prompt 設計,可以讓你的「AI 設計師」App 穩定地從 Gemini API 獲得格式固定的 JSON 顏色數據,從而簡化你的後端處理邏輯。
ColorPaletteViewController
,並根據我們設計的「主色+輔色+強調色」佈局來排版。GeminiDataModels.swift
中,定義一個新的 Codable
結構來解析 AI 回傳的顏色陣列 JSON。Model/Utilities
中建立一個 PromptFactory.swift
,專門用來生成我們精心設計的「調色盤 Prompt」。GeminiAPIService
:讓我們的服務能夠接收不同的 Prompt,變得更加靈活。我們建立一個新的 Xcode 專案,並新增一個 ColorPaletteViewController.swift
及其 .xib
檔案。UI 佈局採用我們討論好的「主色+輔色」三色方案:
一個 UITextField
用於輸入情境描述。
一個 UIButton
用於觸發生成。
三個 UIView
和 六個 UILabel
用於顯示主色、輔助色和強調色及其色碼。
最後,將 UI 元件與程式碼連結。特別的是,我將三個 UIView
、三個用於顯示角色的 UILabel
、以及三個用於顯示色碼的 UILabel
分別連接到三個不同的 IBOutlet Collection
。
我們直接將 Day 19 專案中的網路層程式碼拿到新專案中使用。包含了 APPError.swift
、NetworkManager.swift
和 GeminiDataModels.swift
。(程式碼同 Day 19 文章,此處不再贅述)
為了不讓複雜的 Prompt 字串污染我們的 Service
層,我們建立一個專門的檔案來生產它。
檔案:PromptFactory.swift
(新增)
import Foundation
enum PromptFactory {
static func createColorPalettePrompt(from description: String) -> String {
return """
你是一個專業的色彩分析 AI 助理,專門根據文字描述生成相應的顏色配色方案。
**任務目標:**
根據使用者提供的描述,分析其中的情感、氛圍、季節、物體等元素,生成一個協調且符合描述的 3 色配色方案。
**輸出規範:**
- 格式:純 JSON 陣列,無其他文字
- 內容:恰好 3 個十六進位顏色代碼
- 格式:每個顏色代碼必須以 # 開頭,後接 6 位大寫十六進位數字
- 順序:按照主色調到輔助色調排列
**分析步驟:**
1. 識別描述中的關鍵詞(情感、物體、季節、時間等)
2. 確定主要色調和情感基調
3. 選擇 1 個主色
4. 搭配 1 個輔助色
5. 添加 1 個強調色
6. 確保色彩平衡和視覺協調性
**使用者描述:**
\(description)
**輸出範例:**
["#2E86AB", "#A23B72", "#F18F01"]
"""
}
}
GeminiAPIService
我們將 Day 19 建立的 GeminiAPIService
完全複製過來,不需要做任何修改,只需要傳入由 PromptFactory
生成的新 Prompt 即可。(程式碼同 Day 19 文章,此處不再贅述)
最後,我們在 ColorPaletteViewController
中,將所有部分串連起來。
import UIKit
class ColorPaletteViewController: UIViewController {
// MARK: - IBOutlet
@IBOutlet weak var txfDescription: UITextField!
@IBOutlet weak var btnSent: UIButton!
@IBOutlet var vColors: [UIView]!
@IBOutlet var lbColors: [UILabel]!
@IBOutlet var lbColorsText: [UILabel]!
// MARK: - Property
private let geminiService = GeminiAPIService()
// MARK: - LifeCycle
override func viewDidLoad() {
super.viewDidLoad()
}
// MARK: - IBAction
@IBAction func generateButtonTapped(_ sender: UIButton) {
// 確保輸入框有文字
guard let description = txfDescription.text, !description.isEmpty else {
print("錯誤:請輸入顏色描述")
return
}
// 收起鍵盤並更新按鈕狀態,提供基本的操作回饋
view.endEditing(true)
btnSent.isEnabled = false
btnSent.setTitle("生成中...", for: .normal)
// 使用 Task 執行非同步網路請求
Task {
// 預設的錯誤訊息
var errorMessage: String? = "發生未知錯誤"
do {
// 建立 Prompt
let prompt = PromptFactory.createColorPalettePrompt(from: description)
// 呼叫 Service
let response = try await geminiService.generateContent(prompt: prompt)
// 解析 AI 回應中的 JSON 字串
if let colorJsonString = response.candidates.first?.content.parts.first?.text {
// 清理字串,只保留 JSON 陣列的部分
let cleanJsonString = extractJsonString(from: colorJsonString)
// 將 JSON 字串解碼成 Swift 的字串陣列
if let jsonData = cleanJsonString.data(using: .utf8),
let colorArray = try? JSONDecoder().decode([String].self, from: jsonData) {
// 成功解析,清除錯誤訊息並準備更新 UI
errorMessage = nil
await MainActor.run {
updateColorViews(with: colorArray)
}
} else {
errorMessage = "無法解析AI回傳的顏色數據"
}
} else {
errorMessage = "AI沒有返回有效內容"
}
} catch {
errorMessage = "網路或API錯誤: \(error.localizedDescription)"
}
// 無論成功或失敗,最後都回到主執行緒更新按鈕狀態
await MainActor.run {
btnSent.isEnabled = true
btnSent.setTitle("生成配色", for: .normal)
// 如果有錯誤訊息,印出來
if let error = errorMessage {
print("\(error)")
}
}
}
}
// MARK: - Private Methods
/// 更新顏色 UIView 和 UILabel
private func updateColorViews(with hexStrings: [String]) {
let colorRoles = ["主色", "輔助色", "強調色"]
for (index, hexString) in hexStrings.enumerated() {
guard index < vColors.count && index < lbColors.count && index < lbColorsText.count else { continue }
let color = UIColor(hex: hexString)
vColors[index].backgroundColor = color
lbColors[index].text = colorRoles[index]
lbColorsText[index].text = hexString.uppercased()
}
}
/// 從可能包含額外文字的字串中,提取出 JSON 陣列的部分
private func extractJsonString(from text: String) -> String {
// 尋找第一個 "[" 和最後一個 "]"
if let startIndex = text.firstIndex(of: "["),
let endIndex = text.lastIndex(of: "]") {
// 擷取出 JSON 陣列的子字串
return String(text[startIndex...endIndex])
}
// 如果找不到,直接回傳原字串嘗試解析
return text
}
}
// MARK: - Extensions
extension UIColor {
convenience init(hex: String) {
let hex = hex.trimmingCharacters(in: CharacterSet.alphanumerics.inverted)
var int: UInt64 = 0
Scanner(string: hex).scanHexInt64(&int)
let a, r, g, b: UInt64
switch hex.count {
case 6: // RGB (例如: "FF5733")
(a, r, g, b) = (255, int >> 16, int >> 8 & 0xFF, int & 0xFF)
case 8: // ARGB (例如: "FFFF5733")
(a, r, g, b) = (int >> 24, int >> 16 & 0xFF, int >> 8 & 0xFF, int & 0xFF)
default:
(a, r, g, b) = (255, 0, 0, 0) // 預設為紅色
}
self.init(
red: CGFloat(r) / 255,
green: CGFloat(g) / 255,
blue: CGFloat(b) / 255,
alpha: CGFloat(a) / 255
)
}
}
SceneDelegate.swift
為了讓 App 啟動時顯示我們的新畫面,需要修改 SceneDelegate.swift
:
// 檔案:SceneDelegate.swift
func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) {
guard let windowScene = (scene as? UIWindowScene) else { return }
let rootVC = ColorPaletteViewController(nibName: "ColorPaletteViewController", bundle: nil)
let navigationController = UINavigationController(rootViewController: rootVC)
window = UIWindow(frame: windowScene.coordinateSpace.bounds)
window?.windowScene = windowScene
window?.rootViewController = navigationController
window?.makeKeyAndVisible()
}
!!務必燒到實體機使用,不要用虛擬機測試!!因為 iOS 模擬器本身就是一台虛擬機,在虛擬機裡執行它會造成巢狀虛擬,導致網路功能完全失效而無法連線。
今天,我們成功的指揮 AI 工作。我們也學會了提示工程 (Prompt Engineering) 這個核心技巧,透過精確的指令,讓 AI 穩定地輸出我們需要的 JSON 格式。
明天,我們將挑戰一個更複雜的專案:從零開始,打造一個能將夢境畫出來的「AI 夢境視覺化師」!